#%%[markdown]
""" 
# 贷款风险评估模型
"""
#%%

import pandas as pd
import numpy as np
import datetime
from matplotlib import pyplot as plt


plt.rcParams["font.family"] = "Microsoft YaHei"

#%%[markdown]
"""
# 数据准备 
## 导入数据
"""
#%%
data = pd.read_csv("车贷违约预测.csv", encoding="gbk")
data.info()
data
#%% [markdown]
"""
| 类别     |   子类别          | 客户信息                     |
| -------- | ------------ | ---------------------------- |
| 客户信息 | 基础信息     | 客户编号                     |
| 客户信息 | 基础信息     | 出生日期                     |
| 客户信息 | 基础信息     | 地区                         |
| 客户信息 | 基础信息     | 工作类型                     |
| 客户信息 | 信用信息     | 是否填写手机号               |
| 客户信息 | 信用信息     | 受否填写身份证               |
| 客户信息 | 信用信息     | 是否出具驾驶证               |
| 客户信息 | 信用信息     | 是否填写护照                 |
| 客户信息 | 信用信息     | 信用评分                     |
| 贷款信息 | 品牌方       | 品牌                         |
| 贷款信息 | 品牌方       | 骑车销售商                   |
| 贷款信息 | 品牌方       | 车厂                         |
| 贷款信息 | 审核信息     | 对接员工编号                 |
| 贷款信息 | 审核信息     | 货款日期                     |
| 贷款信息 | 贷款综合信息 | 资产成本                     |
| 贷款信息 | 贷款综合信息 | 贷款与资产比列               |
| 贷款信息 | 贷款综合信息 | 贷款与资产比                 |
| 贷款信息 | 贷款综合信息 | 贷款与已还贷款比列           |
| 贷款信息 | 贷款综合信息 | 贷款与已批准贷款比列         |
| 贷款信息 | 贷款综合信息 | 平均贷款期限                 |
| 贷款信息 | 贷款综合信息 | 第一次贷款距今时间           |
| 贷款信息 | 贷款综合信息 | 贷款查询次数                 |
| 贷款信息 | 批准         | 已批准贷款总额               |
| 贷款信息 | 批准         | 主账户中已批准的贷款         |
| 贷款信息 | 批准         | 次账户中已批准贷款           |
| 贷款信息 | 未批准       | 已发货款                     |
| 贷款信息 | 未批准       | 已发放贷款总额               |
| 贷款信息 | 未批准       | 主账户中已发放贷款           |
| 贷款信息 | 未批准       | 次账户中已发放贷款           |
| 贷款信息 | 还款信息     | 尚未还清有效贷款总额         |
| 贷款信息 | 还款信息     | 主账户中尚未还清有效贷款     |
| 贷款信息 | 还款信息     | 次账户中尚未还清有效贷款     |
| 贷款信息 | 还款信息     | 每月还款总额                 |
| 贷款信息 | 还款信息     | 主账户每月还款               |
| 贷款信息 | 还款信息     | 次账户没用还款               |
| 贷款信息 | 还款信息     | 主账户还款期数               |
| 贷款信息 | 还款信息     | 次账户还款期数               |
| 贷款信息 | 贷款申请次数 | 总贷款次数与总有效贷款次数比 |
| 贷款信息 | 贷款申请次数 | 贷款总次数                   |
| 贷款信息 | 贷款申请次数 | 主账户贷款次数               |
| 贷款信息 | 贷款申请次数 | 次账户贷款次数               |
| 贷款信息 | 贷款申请次数 | 近六个月新贷款次数           |
| 贷款信息 | 有效贷款     | 主账户有效贷款次数           |
| 贷款信息 | 有效贷款     | 次账户有效贷款次数           |
| 贷款信息 | 无效贷款     | 主账户无效贷款次数           |
| 贷款信息 | 无效贷款     | 次账户无效贷款次数           |
| 贷款信息 | 无效贷款     | 无效贷款总次数               |
| 贷款信息 | 违约信息     | 近六个月违约次数             |
| 贷款信息 | 违约信息     | 是否违约                     |


如果希望确定对接员工是否可能出现审批纰漏，需要对其进行哑变量处理

没有重复的用户,不需要进行去重
"""
#%%
data[data["客户编号"].duplicated()]

#%% [markdown]
""" 
数据探索

对审批人员构建哑变量或者标签非常有必要

可以看出近一半的员工贡献了 2/3的违约

近2/3的员工贡献了几乎全部错误，

审核人员合格率因低于15%左右为佳
"""
#%%


def id_relate(data, key="对接员工编号"):
    n = (
        data.groupby(by=key)
        .agg({"客户编号": "count", "是否违约": "sum", "近六个月新贷款次数": "sum", "近六个月违约次数": "sum"})
        .sort_values("客户编号", ascending=False)
    )
    n["违约率"] = n["是否违约"] / n["客户编号"]
    n["6违约率"] = n["近六个月违约次数"] / n["近六个月新贷款次数"]
    n["30%"] = n["违约率"] > 0.3
    n["30%人数"] = n[n["30%"]]["是否违约"]
    n["20%"] = n["违约率"] > 0.2
    n["20%人数"] = n[n["20%"]]["是否违约"]
    n["18%"] = n["违约率"] > 0.18
    n["18%人数"] = n[n["18%"]]["是否违约"]
    n["17%"] = n["违约率"] > 0.17
    n["17%人数"] = n[n["17%"]]["是否违约"]
    n["16%"] = n["违约率"] > 0.16
    n["16%人数"] = n[n["16%"]]["是否违约"]
    n["15%"] = n["违约率"] > 0.1
    n["15%人数"] = n[n["15%"]]["是否违约"]
    return (
        n[
            [
                "30%",
                "30%人数",
                "20%",
                "20%人数",
                "18%",
                "18%人数",
                "17%",
                "17%人数",
                "16%",
                "16%人数",
                "15%",
                "15%人数",
                "是否违约",
            ]
        ].sum(),
        n.shape,
        n[(n["客户编号"] > 1) & (n["违约率"] != 0)][
            ["客户编号", "近六个月新贷款次数", "近六个月违约次数", "是否违约", "违约率", "6违约率"]
        ].sort_values("违约率", ascending=False),
    )


#%%
n1, n2, ny3 = id_relate(data, key=["对接员工编号"])
# ny3.reset_index(inplace=True)
ny3
#%%
n1, n2, nj3 = id_relate(data, key=["骑车销售商"])
# nj3.reset_index(inplace=True)
nj3
#%%
id_relate(data, key=["品牌"])
#%%
id_relate(data, key=["地区"])  # 确实存在高危地区，地区
#%% [markdown]
""" 
看样子要破案了，违约存在特定组合，对接人员与经销商可能存在勾结，这些可以考虑优先从审批中排除
"""
#%%
n1, n2, ng3 = id_relate(data, key=["骑车销售商", "对接员工编号"])
n1, n2
ng3.reset_index(inplace=True)
ng3
#%%

dic = ng3[ng3["违约率"] == 1][["骑车销售商", "对接员工编号"]].to_dict("list")
nj3.loc[list(set(dic["骑车销售商"]))].sort_values("违约率", ascending=False)

#%%
ny3.loc[list(set(dic["对接员工编号"]))].sort_values("违约率", ascending=False)


# %% [markdown]
""" 
可以看出数据存在倾向性，建立模型时需要注意
"""
#%%
data["是否违约"].value_counts()
#%%[markdown]
""" 
['客户编号', # 用户序列号不需要考虑在模型中,无意义计算
'品牌', # 不需要观察异常值
'骑车销售商', # 不需要观察异常值
'车厂', # 不需要观察异常值
'出生日期', # 不需要观察异常值
'货款日期', # 不需要观察异常值
'地区' # 不需要观察异常值
"""
#%%
X_col = data.columns.tolist()
data[X_col[0:10]].describe()
#%%[markdown]
""" 
'是否填写手机号', # 0-1 不需要观察异常值
'受否填写身份证', # 0-1 不需要观察异常值
'是否出具驾驶证', # 0-1 不需要观察异常值
'是否填写护照', # 0-1 不需要观察异常值
"""
#%%
data[X_col[10:20]].describe()
#%% [markdown]
data[data["主账户中尚未还清有效贷款"] < 0][["是否违约", "尚未还清有效贷款总额", "已发放贷款总额", "资产成本", "已发货款"]]

#%%
data[X_col[20:30]].describe()

#%% [markdown]
""" 
'是否违约', Y值
"""
#%%
data[X_col[30:40]].describe()
#%%
""" 
['贷款与已还贷款比列'] # 好多大于1或者 还有负数,此列计算有问题
"""
#%%
data[X_col[40:]].describe()
#%% [markdown]
""" 
## 核验数据一致性

### 主次账号与总额数据核算
"""
#%%
# 已批准贷款总额 - 通过核验
data[data["已批准贷款总额"] != data[["主账户中已批准的贷款", "次账户中已批准贷款"]].sum(axis=1)]
# 已发放贷款总额 - 通过核验
data[data["已发放贷款总额"] != data[["主账户中已发放贷款", "次账户中已发放贷款"]].sum(axis=1)]
# 尚未还清有效贷款总额 - 通过核验
data[data["尚未还清有效贷款总额"] != data[["主账户中尚未还清有效贷款", "次账户中尚未还清有效贷款"]].sum(axis=1)][
    ["尚未还清有效贷款总额", "主账户中尚未还清有效贷款", "次账户中尚未还清有效贷款", "是否违约"]
]

# 每月还款总额 - 通过核验
data[data["每月还款总额"] != data[["主账户每月还款", "次账户没用还款"]].sum(axis=1)]
#%%
# 贷款总次数 - 通过核验
data[data["贷款总次数"] != data[["主账户贷款次数", "次账户贷款次数"]].sum(axis=1)]
# 无效贷款总次数 - 通过核验
data[data["无效贷款总次数"] != data[["主账户无效贷款次数", "次账户无效贷款次数"]].sum(axis=1)]
# 贷款次数信息 -- 通过检验
data[
    data["贷款总次数"]
    != data[["主账户有效贷款次数", "次账户有效贷款次数", "主账户无效贷款次数", "次账户无效贷款次数"]].sum(axis=1)
]

# 总贷款次数与总有效贷款次数比 - 未通过核验 -- 需要重算
data[
    data["总贷款次数与总有效贷款次数比"]
    != data["贷款总次数"] / data[["主账户有效贷款次数", "次账户有效贷款次数"]].sum(axis=1)
]["总贷款次数与总有效贷款次数比"]
# %%
data[data["近六个月违约次数"] > data["近六个月新贷款次数"]]  # 无法判断,可能违约发生时间再之前发生
# data[['近六个月违约次数','近六个月新贷款次数']]
data[data["贷款与已批准贷款比列"] != data["已批准贷款总额"] / data["已发放贷款总额"]]["贷款与已批准贷款比列"]
# 存在异常值其中 由于精度丢失所致,建议重算
data[data["贷款与已还贷款比列"] != data["已发放贷款总额"] / data["尚未还清有效贷款总额"]][
    ["贷款与已还贷款比列", "尚未还清有效贷款总额", "已发放贷款总额", "已批准贷款总额"]
]

# data[data['资产成本'] == 0]
# data['已发放贷款总额/资产成本'] = data['资产成本'] /  data[['已批准贷款总额','资产成本']].sum(axis=1)
# data['已发放贷款总额/资产成本']

# data['已批准贷款总额/资产成本'] = data['已批准贷款总额'] / data['资产成本']

# 平均贷款期限 - 未通过核验
# data[data['平均贷款期限'] != data[['主账户还款期数','次账户还款期数']].mean(axis=1)][['平均贷款期限','主账户还款期数','次账户还款期数']]
# data[['主账户还款期数','次账户还款期数']].mean(axis=1) # 期数不正常的也需要进行处理

# 未通过核验 -- 以已发放贷款总额为准?
# data[['已发货款','已发放贷款总额']]

#%% [markdown]
""" 
主账户中尚未还清有效贷款, 次账户中尚未还清有效贷款, 尚未还清有效贷款总额

存在复值,但是无法判断是否为异常,由于之前数据验证过程中发现数值间可以核对上
"""
#%%
# data['主账户中尚未还清有效贷款'] = data['主账户中尚未还清有效贷款'].apply(lambda x:0 if x < 0 else x)
# data['次账户中尚未还清有效贷款'] = data['次账户中尚未还清有效贷款'].apply(lambda x:0 if x < 0 else x)
# data['尚未还清有效贷款总额'] = data['尚未还清有效贷款总额'].apply(lambda x:0 if x < 0 else x)

data[data["已批准贷款总额"] < data["尚未还清有效贷款总额"]][["已批准贷款总额", "已发放贷款总额", "已发货款"]]
#%%
data[["贷款与资产比列", "贷款与资产比"]]  # 两者相似

data = pd.merge(
    data,
    pd.get_dummies(data["工作类型"]).rename(columns={0: "工作0", 1: "工作1", 2: "工作2"}),
    left_index=True,
    right_index=True,
)

np.isinf(data).sum()
#%% [markdown]
""" 
无效或异常值处理

货款比例中出现了inf,由于知道如何计算,后续将对其进行重新计算并替换字段

货款日期均为2018,对模型没有帮助,因此删除
"""
#%%
data.drop(columns=["贷款与已还贷款比列", "已发货款"], inplace=True)
data

# %%[markdown]
""" 
贷款时年龄 = 出生日期 - 货款日期

无效贷款总次数 =  无效贷款总次数 / 贷款总次数

还贷率 = 已发放贷款总额 / 尚未还清有效贷款总额

有效信息数 = ['客户编号','资产成本','出生日期','地区','是否填写手机号','受否填写身份证'
            ,'是否出具驾驶证','是否填写护照','信用评分'].sum()

经销商违约率 = groupby加上去

对接员工违约率 = groupby也加上去

对接员工编号_对接员工违约率 = 两个违约率GB匹配上去

工作类型进行独热编码
"""
#%%
# 之所以求无效率,理由是无效越大,发生违约的可能越大,另外天生就在01范围内,有利于xgboost或其他树模型
data["无效贷款总次数/总贷款次数(贷款被拒)"] = data["无效贷款总次数"] / data["贷款总次数"].apply(
    lambda x: x if x != 0 else 1
)
data["无效贷款总次数/总贷款次数(贷款被拒)"]

data["尚未还清贷款/已发放贷款总额"] = data["尚未还清有效贷款总额"] / data["已发放贷款总额"].apply(
    lambda x: x if x != 0 else 1
)
data[data["尚未还清贷款/已发放贷款总额"] > 1]

data["用户年龄"] = data["货款日期"] - data["出生日期"]
data["用户年龄"].describe()

data["有效信息数"] = data[["是否填写手机号", "受否填写身份证", "是否出具驾驶证", "是否填写护照"]].sum(axis=1)
data["有效信息数"]


data.drop(columns=["货款日期"], inplace=True)
data
#%%
# 骑车销售商, 对接员工编号
def id_relate(data, key=["对接员工编号"]):
    n = (
        data.groupby(by=key)
        .agg({"客户编号": "count", "是否违约": "sum", "近六个月违约次数": "sum"})
        .sort_values("客户编号", ascending=False)
    )
    keyn = "_".join(key)
    n[keyn + "_违约率"] = n["是否违约"] / n["客户编号"]
    n.rename(columns={"近六个月违约次数": keyn + "_近六个月违约客户"}, inplace=True)
    n.reset_index(inplace=True)
    l = [keyn + "_违约率"]
    l.extend(key)
    n = pd.merge(data, n[l], on=key, how="outer")
    return n


data = id_relate(data)
data = id_relate(data, key=["骑车销售商"])
data = id_relate(data, key=["对接员工编号", "骑车销售商"])
data = id_relate(data, key=["品牌"])
data = id_relate(data, key=["车厂"])
data = id_relate(data, key=["地区"])
#%%
data.info()

#%%
data.drop(
    columns=["骑车销售商", "对接员工编号", "品牌", "车厂", "地区", "出生日期", "贷款与资产比列"], inplace=True
)

# %% [markdown]
""" 
各项数据之间的分布情况对是否违约并不存在非常显著的差异，
"""
#%%
bins = 50
l = data.columns.tolist()
# l.remove("贷款与已还贷款比列")
l2 = {}
for key in l:
    dic = {}
    plt.figure(figsize=(12, 3))
    plt.hist(data[(data["是否违约"] == 0)][key], bins=bins)
    plt.hist(data[(data["是否违约"] == 1)][key], bins=bins)
    plt.title(key)
#%%
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import (
    GridSearchCV,
    cross_val_predict,
    cross_val_score,
    train_test_split,
)
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier
import xgboost as xgb
from xgboost.sklearn import XGBClassifier
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score


def resout(module, X, Y):
    yp = module.predict(X)
    print(classification_report(Y, yp))
    print(confusion_matrix(Y, yp, labels=[0, 1]))


#%% 样本存在严重不均衡，需要在模型中使用class_weight="balanced" 或 调整样本比例
t = data["是否违约"].value_counts()
t[1] / t[0]  # 0.215 可以选取为 0.2
#%%
data.set_index("客户编号", inplace=True)

t_data = pd.DataFrame(MinMaxScaler().fit_transform(data), columns=data.columns)

Y = data["是否违约"]
X = data.drop(columns="是否违约")
# %%
X_train, X_test, Y_train, Y_test = train_test_split(
    X, Y, shuffle=True, random_state=42, test_size=0.3
)

#%% [markdown]
""" 
逻辑回归 0.28
"""
#%%
LR = LogisticRegression(penalty="l2", class_weight="balanced", n_jobs=3)

lr = LR.fit(X_train, Y_train)

print(resout(lr, X_train, Y_train))
lr.score(X_train, Y_train)
#%%
print(resout(lr, X_test, Y_test))
lr.score(X_test, Y_test)  # 核心问题在于1类准确率过低
#%%[markdown]
""" 
决策树
"""
#%%
rft = RandomForestClassifier(
    n_estimators=100, criterion="entropy", class_weight="balanced", n_jobs=-1
)
rft.fit(X_train, Y_train)
print(resout(rft, X_train, Y_train))
print(resout(rft, X_test, Y_test))
#%%
param_grid = {
    "criterion": ["entropy"],
    "min_samples_split": [5, 10],
    "max_features": [30],
    "max_depth": [8 + i for i in range(3)],
    # "min_impurity_decrease": [0 + i for i in range(5)],
    # 'min_samples_leaf':[i+1 for i in range(2)]
}

rftgv = GridSearchCV(rft, param_grid, n_jobs=-1, cv=5, scoring="roc_auc")
rftgv.fit(X, Y)
rftgv.best_params_, rftgv.best_estimator_, rftgv.best_score_
#%%
rft = rftgv.best_estimator_
rft.fit(X_train, Y_train)
print(resout(rft, X_train, Y_train))
print(resout(rft, X_test, Y_test))
# %%[markdown]
""" 
召回率显著提高，但是1类的准确率依然较低，总体准确率0.66左右（主要0类错误较多）
模型缺乏实用性，还需改进
"""

#%%[markdown]
""" 
XGB
"""
#%%
XGB = XGBClassifier(
    booster="gbtree",
    # objective="binary:logistic",
    metric="auc",
    eval_metric="auc",
    scale_pos_weight=0.3,
)
XGB.fit(X_train, Y_train)
#%%
print(resout(XGB, X_train, Y_train))
#%%
print(resout(XGB, X_test, Y_test))

#%%

param_grid = {
    "verbosity": [1],
    "eta": [0.2],#[0.2, 0.3]
    "max_depth": [3 + i * 2 for i in range(5)],
    "min_child_weight": [2],# [1 + i for i in range(3)]
    "learning_rate": [0.2 + 0.1 * (0 + i) for i in range(3)],
    "subsample": [0.7],
    "colsample_bytree": [0.1 * i + 0.4 for i in range(2)],
}

xgbgs = GridSearchCV(XGB, param_grid, n_jobs=3, cv=5)
xgbgs.fit(X_train, Y_train)
xgbgs.best_params_, xgbgs.best_score_
#%%
XGB = xgbgs.best_estimator_
XGB.fit(X_train, Y_train)
#%%
print(resout(XGB, X_train, Y_train))
#%%
print(resout(XGB, X_test, Y_test))
#%%
''' 
xgboost 效果略好于 随机森林
因此模型使用xgboost，但是投入生产前需要考虑实际使用情况
'''
# %%[markdwon]
""" 
模型导出
"""
#%%
import joblib

joblib.dump(XGB, "model.model")

# %%
